// This file is part of Notepad4.
// See License.txt for details about distribution and modification.
//! Lexer for GNU Texinfo.

#include <cassert>
#include <cstring>

#include <string>
#include <string_view>

#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"

#include "WordList.h"
#include "LexAccessor.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "StringUtils.h"
#include "LexerModule.h"

using namespace Lexilla;

namespace {

//KeywordIndex++Autogenerated -- start of section automatically generated
enum {
	KeywordIndex_Command = 0,
	KeywordIndex_BlockCommand = 1,
	MaxKeywordSize = 32,
};
//KeywordIndex--Autogenerated -- end of section automatically generated

enum class KeywordType {
	None = SCE_TEXINFO_DEFAULT,
	End = SCE_TEXINFO_END_TAG,
	Macro = SCE_TEXINFO_MARCO,
};

enum {
	TexinfoLineStateTeX = 1,
	TexinfoLineStateLaTeX = 2,
	TexinfoLineStateDisplayMath = 3,
	TexinfoLineStateMacro = 4,
	TexinfoLineStateRMacro = 8,
	TexinfoLineStateMask = 15,
	TexinfoLineStateTeXMask = TexinfoLineStateMacro - 1,
	TexinfoLineStateMacroMask = TexinfoLineStateMacro | TexinfoLineStateRMacro,
};

constexpr bool IsTexiSpecial(int ch) noexcept {
	// https://www.gnu.org/software/texinfo/manual/texinfo/html_node/Command-Syntax.html
	// treat space and punctuation as special
	return ch <= ' ' || (ch < 127 && !IsADigit(ch));
}

constexpr bool IsTitleLine(int state) noexcept {
	return state >= SCE_TEXINFO_TITLE;
}

constexpr bool InsideTex(int lineState) noexcept {
	return (lineState & TexinfoLineStateTeXMask) != 0;
}

void HandleBlockEnd(StyleContext &sc, LexAccessor &styler, int &lineState) {
	if (styler[sc.currentPos + 2] == 'n' && styler[sc.currentPos + 3] == 'd') {
		static constexpr char wordList[][12] = {
			"tex",
			"latex",
			"displaymath",
			"macro",
			"rmacro",
			"ignore",
			"verbatim",
		};
		constexpr unsigned ignore = TexinfoLineStateDisplayMath + 2;
		unsigned index;
		if (sc.state != SCE_TEXINFO_DEFAULT) {
			index = sc.state - SCE_TEXINFO_COMMENT2 + ignore;
		} else {
			index = lineState & TexinfoLineStateTeXMask;
			if (index) {
				index = index - 1;
			} else {
				index = TexinfoLineStateDisplayMath + (lineState >> 3);
			}
		}

		const char *word = wordList[index];
		const Sci_PositionU startPos = sc.currentPos + CStrLen("@end");
		for (Sci_PositionU pos = startPos; pos < sc.lineStartNext; pos++) {
			char ch = styler[pos];
			if (ch == word[0]) {
				if (pos > startPos) {
					do {
						++word;
						++pos;
						ch = styler[pos];
					} while (*word && *word == ch);
					if (*word == '\0' && !IsAlpha(ch)) {
						if (index < TexinfoLineStateDisplayMath) {
							lineState = lineState & TexinfoLineStateMacroMask;
						} else if (index < ignore) {
							lineState = 0;
						}
						sc.SetState(SCE_TEXINFO_COMMAND);
						return;
					}
				}
				break;
			}
			if (!IsSpaceOrTab(ch)) {
				break;
			}
		}
	}
	if (sc.state == SCE_TEXINFO_DEFAULT && !InsideTex(lineState)) {
		sc.SetState(SCE_TEXINFO_COMMAND);
	}
}

void ColouriseTexiDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int initStyle, LexerWordList keywordLists, Accessor &styler) {
	const bool fold = styler.GetPropertyBool("fold");

	int visibleChars = 0;
	int outerState = SCE_TEXINFO_DEFAULT;
	int braceCount = 0;
	int mathBraceCount = 0;
	int headerLevel = 0;
	int levelCurrent = SC_FOLDLEVELBASE;
	KeywordType kwType = KeywordType::None;
	int delimiter = 0;
	int lineState = 0;

	StyleContext sc(startPos, lengthDoc, initStyle, styler);
	if (sc.currentLine > 0) {
		levelCurrent = styler.LevelAt(sc.currentLine - 1) >> 16;
		lineState = styler.GetLineState(sc.currentLine - 1);
		braceCount = lineState >> 8;
		lineState &= TexinfoLineStateMask;
	}
	if (startPos == 0 && sc.Match('\\', 'i')) {
		// \input at document beginning
		sc.SetState(SCE_TEXINFO_TEX_COMMAND);
		sc.Forward();
	}

	int levelNext = levelCurrent;
	while (sc.More()) {
		if (sc.atLineStart) {
			if (sc.state == SCE_TEXINFO_COMMENT || IsTitleLine(sc.state)) {
				if (sc.state != SCE_TEXINFO_COMMENT) {
					outerState = SCE_TEXINFO_DEFAULT;
					braceCount = 0;
				}
				sc.SetState(SCE_TEXINFO_DEFAULT);
			}
		}

		switch (sc.state) {
		case SCE_TEXINFO_OPERATOR:
		case SCE_TEXINFO_SPECIAL:
			sc.SetState(braceCount ? SCE_TEXINFO_DEFAULT : outerState);
			break;

		case SCE_TEXINFO_COMMAND:
			if (!IsAlpha(sc.ch)) {
				char s[MaxKeywordSize];
				sc.GetCurrent(s, sizeof(s));
				const char *p = s + 1;
				int state = braceCount ? SCE_TEXINFO_DEFAULT : outerState;
				kwType = KeywordType::None;
				if (keywordLists[KeywordIndex_BlockCommand].InList(p)) {
					levelNext++;
					braceCount = 0;
					if (StrEqual(p, "ignore")) {
						state = SCE_TEXINFO_COMMENT2;
					} else if (StrEqual(p, "verbatim")) {
						state = SCE_TEXINFO_VERBATIM2;
					} else if (StrEqual(p, "macro")) {
						kwType = KeywordType::Macro;
						lineState = TexinfoLineStateMacro;
					} else if (StrEqual(p, "rmacro")) {
						kwType = KeywordType::Macro;
						lineState = TexinfoLineStateRMacro;
					} else if (StrEqual(p, "tex")) {
						lineState = (lineState & TexinfoLineStateMacroMask) | TexinfoLineStateTeX;
					} else if (StrEqual(p, "latex")) {
						lineState = (lineState & TexinfoLineStateMacroMask) | TexinfoLineStateLaTeX;
					} else if (StrEqual(p, "displaymath")) {
						lineState = (lineState & TexinfoLineStateMacroMask) | TexinfoLineStateDisplayMath;
					}
				} else if (!keywordLists[KeywordIndex_Command].InList(p)) {
					sc.ChangeState(SCE_TEXINFO_MARCO);
				} else if (StrEqualsAny(p, "c", "comment")) {
					sc.ChangeState(SCE_TEXINFO_COMMENT);
				} else if (StrEqual(p, "end")) {
					levelNext--;
					kwType = KeywordType::End;
					state = SCE_TEXINFO_DEFAULT;
					braceCount = 0;
				} else if (StrEqualsAny(p, "chapter", "chapheading", "majorheading")) {
					state = SCE_TEXINFO_CHAPTER;
				} else if (StrEqualsAny(p, "section", "heading")) {
					state = SCE_TEXINFO_SECTION;
				} else if (StrEqualsAny(p, "subsection", "subheading")) {
					state = SCE_TEXINFO_SECTION1;
				} else if (StrEqualsAny(p, "subsubsection", "subsubheading")) {
					state = SCE_TEXINFO_SECTION2;
				} else if (StrEqualsAny(p, "title", "settitle", "top")) {
					state = SCE_TEXINFO_TITLE;
				} else if (StrStartsWith(p, "appendix") || StrStartsWith(p, "unnumbered")) {
					p += (p[0] == 'a') ? CStrLen("appendix") : CStrLen("unnumbered");
					if (*p == '\0')	{
						state = SCE_TEXINFO_CHAPTER;
					} else if (StrEqualsAny(p, "sec", "section")) {
						state = SCE_TEXINFO_SECTION;
					} else if (StrEqual(p, "subsec")) {
						state = SCE_TEXINFO_SECTION1;
					} else if (StrEqual(p, "subsubsec")) {
						state = SCE_TEXINFO_SECTION2;
					}
				} else if (StrEqual(p, "verb")) {
					if (sc.ch == '{' && sc.chNext != '}' && IsAGraphic(sc.chNext)) {
						state = SCE_TEXINFO_OPERATOR;
						delimiter = sc.chNext;
					}
				} else if (StrEqual(p, "math")) {
					if (sc.ch == '{') {
						mathBraceCount = braceCount + 1;
					}
				}
				if (sc.state != SCE_TEXINFO_COMMENT) {
					if (IsTitleLine(state)) {
						outerState = state;
						braceCount = 0;
						lineState = 0;
						headerLevel = state - SCE_TEXINFO_TITLE;
					}
					sc.SetState(state);
					if (state == SCE_TEXINFO_OPERATOR) {
						sc.ForwardSetState(SCE_TEXINFO_VERBATIM);
					}
				}
			}
			break;

		case SCE_TEXINFO_TEX_COMMAND:
		case SCE_TEXINFO_END_TAG:
		case SCE_TEXINFO_MARCO:
			if (!IsAlpha(sc.ch) && (sc.state != SCE_TEXINFO_MARCO || !IsADigit(sc.ch))) {
				if (sc.state == SCE_TEXINFO_MARCO && sc.ch == '\\') {
					sc.Forward();
				}
				sc.SetState(braceCount ? SCE_TEXINFO_DEFAULT : outerState);
			}
			break;

		case SCE_TEXINFO_VERBATIM:
			if (sc.chNext == '}' && sc.ch == delimiter) {
				sc.ForwardSetState(SCE_TEXINFO_OPERATOR);
				sc.ForwardSetState(braceCount ? SCE_TEXINFO_DEFAULT : outerState);
			}
			break;

		case SCE_TEXINFO_COMMENT2:
		case SCE_TEXINFO_VERBATIM2:
			if (visibleChars == 0 && sc.Match('@', 'e')) {
				HandleBlockEnd(sc, styler, lineState);
			}
			break;
		}

		if (sc.state == SCE_TEXINFO_DEFAULT || IsTitleLine(sc.state)) {
			if (kwType != KeywordType::None && sc.ch > ' ') {
				const int state = static_cast<int>(kwType);
				kwType = KeywordType::None;
				if (IsAlpha(sc.ch)) {
					sc.SetState(state);
					continue;
				}
			}
			if (lineState != 0 && visibleChars == 0 && sc.Match('@', 'e')) {
				HandleBlockEnd(sc, styler, lineState);
			} else if (sc.ch == '@' && !InsideTex(lineState)) {
				if (IsAlpha(sc.chNext)) {
					sc.SetState(SCE_TEXINFO_COMMAND);
				} else if (IsTexiSpecial(sc.chNext)) {
					sc.SetState(SCE_TEXINFO_SPECIAL);
					if (!IsEOLChar(sc.chNext)) {
						sc.Forward();
					}
				}
			} else if (sc.ch == '{' || sc.ch == '}') {
				sc.SetState(SCE_TEXINFO_OPERATOR);
				if (sc.ch == '{') {
					++braceCount;
				} else if (braceCount > 0) {
					if (braceCount == mathBraceCount) {
						mathBraceCount = 0;
					}
					--braceCount;
					if (braceCount == 0 && outerState != SCE_TEXINFO_DEFAULT) {
						sc.ForwardSetState(outerState);
						continue;
					}
				}
			} else if (sc.Match('`', '`') || sc.Match('\'', '\'')) {
				sc.SetState(SCE_TEXINFO_SPECIAL);
				sc.Forward();
			} else if (lineState != 0 || mathBraceCount != 0) {
				if (sc.ch == '\\') {
					if (IsAlpha(sc.chNext)) {
						sc.SetState((lineState < TexinfoLineStateMacro) ? SCE_TEXINFO_TEX_COMMAND : SCE_TEXINFO_MARCO);
					} else if (IsAGraphic(sc.chNext) && !IsADigit(sc.chNext)) {
						sc.SetState(SCE_TEXINFO_SPECIAL);
						sc.Forward();
					}
				} else if (lineState != 0) {
					if (sc.ch == '%' && InsideTex(lineState)) {
						sc.SetState(SCE_TEXINFO_COMMENT);
					}
				}
			}
		}

		if (visibleChars == 0 && !isspacechar(sc.ch)) {
			++visibleChars;
		}
		if (sc.atLineEnd) {
			if (fold) {
				if (headerLevel) {
					levelNext = headerLevel + SC_FOLDLEVELBASE;
					levelCurrent = levelNext - 1;
					headerLevel = 0;
				}
				levelNext = sci::max(levelNext, SC_FOLDLEVELBASE);
				const int levelUse = levelCurrent;
				int lev = levelUse | (levelNext << 16);
				if (levelUse < levelNext) {
					lev |= SC_FOLDLEVELHEADERFLAG;
				}
				styler.SetLevel(sc.currentLine, lev);
				levelCurrent = levelNext;
			}

			styler.SetLineState(sc.currentLine, lineState | (braceCount << 8));
			visibleChars = 0;
			kwType = KeywordType::None;
			mathBraceCount = 0;
		}
		sc.Forward();
	}

	sc.Complete();
}

}

extern const LexerModule lmTexinfo(SCLEX_TEXINFO, ColouriseTexiDoc, "texi");
